///////////////////////////////////////////////////////////////////////////////
//
//  Copyright (C) 2007-2009 ILOG, S.A.
//  All Rights Reserved.
//  The following is ILOG Source Code.  No modifications may be made to the  
//  ILOG Source Code.  Any usage of the ILOG Source Code is subject to 
//  the terms and conditions of the ILOG End User License Agreement   
//  applicable to this ILOG software product.
//
///////////////////////////////////////////////////////////////////////////////

package ilog.gauges.gaugesClasses.circular
{
  import flash.display.DisplayObject;
  import flash.events.Event;
  import flash.events.MouseEvent;
  import flash.geom.Point;
  
  import ilog.utils.CSSUtil;
  import ilog.utils.MathUtil;
  
  import mx.effects.AnimateProperty;
  import mx.effects.easing.Quintic;
  import mx.events.EffectEvent;
  import mx.events.FlexEvent;
  import mx.events.SandboxMouseEvent;
  import mx.styles.CSSStyleDeclaration;

  /**
   * Dispatched when the value in the <code>CircularValueRenderer</code> changes
   * through user input.
   * This event does not occur if you use data binding or 
   * ActionScript code to change the value.
   *
   * <p>Even though the default value of the <code>Event.bubbles</code> property 
   * is <code>true</code>, this control dispatches the event with 
   * the <code>Event.bubbles</code> property set to <code>false</code>.</p>
   *
   * @eventType flash.events.Event.CHANGE
   */
  [Event(name="change", type="flash.events.Event")]

  /**
   * The length in milliseconds, 
   * of a transition when changing the <code>value</code>. 
   * 
   * @default 500
   */
  [Style(name="animationDuration", type="Number", format="Time", inherit="no")]  

  /**
   * The easing function for the animation.
   * The easing function is used to interpolate when animating
   * the value on value changed.
   * A trivial easing function does linear interpolation.
   * More sophisticated easing functions create the illusion of
   * acceleration and deceleration, which makes the animation seem
   * more natural.
   * 
   * @see mx.effects.Tween#easingFunction
   * 
   * @default Quintic.easeInOut
   */
  [Style(name="easingFunction", type="Function", inherit="no")]   

  /**
   * The <code>CircularValueRenderer</code> is the base class for visual circular 
   * elements that renders a value relative to a scale. 
   *  
   * @mxml
   *  
   * <p>The <code>&lt;CircularValueRenderer&gt;</code> tag inherits all the properties
   * of its parent classes and adds the following properties:</p>
   *  
   * <pre>
   * &lt;CircularValueRenderer
   *   <strong>Properties</strong>
   *   value="<i>null</i>"
   *   animationOrientation="normal"
   *   transitionAngle="NaN"
   *   editable="false|true"
   *   editMode="discrete|continuous"
   *   mouseMode="shape|area"
   *   
   *   <strong>Styles</strong>
   *   animationDuration="<i>500</i>"
   *   easingFunction="<i>Quintic.easeInOut</i>"
   *  /&gt;
   * </pre>
   * 
   * @see ilog.gauges.gaugesClasses.circular.ICircularScale
   * @see ilog.gauges.gaugesClasses.circular.CircularGaugeElement
   */   
  public class CircularValueRenderer extends CircularScaleRelatedRenderer
  {
    /**
     *  Constructor
     */    
    public function CircularValueRenderer()
    {
      super();
    }

    /**
     * @private 
     */     
    private static var stylesInited:Boolean = initStyles(); 

    /**
     * @private 
     */ 
    private static function initStyles():Boolean
    {
      var style:CSSStyleDeclaration =
        CSSUtil.createSelector("CircularValueRenderer");   
            
      style.defaultFactory = function():void
        {
          this.easingFunction = Quintic.easeInOut;
          this.animationDuration = 500; 
        }

      return true;
    }

    /**
     *  @private
     */    
    private var _animateProperty:AnimateProperty;

    /**
     *  @private
     */
    private var _keepTransition:Boolean = false;
    
    /**
     *  @private
     */    
    private var _preventAnimation:Boolean = false;

    /**
     *  @private
     */
    private var _value:Object = null;
    
    [Bindable("change")]
    [Bindable("valueCommit")]
    [Inspectable(category="Gauge")]
    
    /**
     * The value rendered by the element. This can change either through user
     * input if <code>editable</code> is <code>true</code> or programmatically
     * by using the property setter.
     * 
     * @default null
     * @see #editable
     * @see #liveDragging
     */
    public function get value():Object
    {
      return _value;
    }

    /**
     *  @private
     */
    public function set value(val:Object):void
    {
      // if the value has not change *and* we do not need to override
      // an existing transitionPosition let's do nothing
      if (_value == val && isNaN(_transitionAngle))
        return;
        
      if (val != null && createTween() != null && scale != null && 
          _value != null && !_preventAnimation) {
        animateTo(scale.angleForValue(val));
      }    
      
      _value = val;
      invalidateDisplayList();
      dispatchEvent(new FlexEvent(FlexEvent.VALUE_COMMIT));
    }
       
    private function animateTo(toAngle:Number):void
    {
      var fromAngle:Number;
      if (_animateProperty.isPlaying) {
        fromAngle = MathUtil.mod2PI(transitionAngle);
        _animateProperty.stop();    
      } else if (!isNaN(transitionAngle)) {
        fromAngle = MathUtil.mod2PI(transitionAngle);
      } else {
        fromAngle = scale.angleForValue(_value);
      }    
      commitProperties();
      
      if (startAngleRadians != endAngleRadians)
      {
        // The rotation effect may have to pass by angle 0° => toAngle < 0 | toAngle > 2PI
        var shift:Number;
          
        // Compute a shift to compute angles difference in interval [0, 2PI] 
        if (scale.orientation == "clockwise" && startAngleRadians < endAngleRadians)
          shift = 2 * Math.PI - endAngleRadians;
        if (scale.orientation == "cclockwise" && startAngleRadians > endAngleRadians)
          shift = - startAngleRadians;
  
        // The rotation have to pass by 0°
        if (!isNaN(shift))
          toAngle = fromAngle + MathUtil.mod2PI(toAngle + shift) - MathUtil.mod2PI(fromAngle + shift);
      }
      else
      {
        var delta1:Number = MathUtil.mod2PI(toAngle - fromAngle)
        var delta2:Number = MathUtil.mod2PI(fromAngle - toAngle);
        var dir:Number = fromAngle < toAngle ? 1 : -1;
        if (delta1 < delta2)
        {
          if (fromAngle > toAngle)
            dir = - dir;
          toAngle = fromAngle + dir * delta1;
        }
        else
        {
          if (fromAngle < toAngle)
            dir = - dir;
          toAngle = fromAngle + dir * delta2;  
        }
         
      }
      _animateProperty.fromValue = fromAngle;
      _animateProperty.toValue = toAngle;
      _animateProperty.play();
    }

    /**
     *  @private
     */
    private var _editable:Boolean = false;
    
    [Inspectable(category="Gauge")]    

    /**
     * Indicates whether the user is allowed to edit the value of the element.
     * If <code>true</code>, the user can drag the element to change its value.
     *
     * @default false
     */    
    public function get editable():Boolean
    {
      return _editable;
    }

    /**
     *  @private
     */
    public function set editable(value:Boolean):void
    {
      if (_editable == value)
        return;
      _editable = value;
      if (_editable) {
        buttonMode = true        
        addEventListener(MouseEvent.MOUSE_DOWN, mouseDownHandler);
      } else {
        removeEventListener(MouseEvent.MOUSE_DOWN, mouseDownHandler);
        buttonMode = false;
      }
      invalidateDisplayList();
    }

    /**
     *  @private
     */
    private var _downX:Number;

    /**
     *  @private
     */
    private var _downY:Number;

    /**
     *  @private
     */
    private var _initialRot:Number;

    /**
     *  @private
     */
    private var _globalOrigin:Point;

    /**
     *  @private
     */
    private var _orientationNum:int;

    /**
     *  @private
     */
    private var _isOutApplication:Boolean;

    /**
     *  @private
     */
    private function mouseDownHandler(e:MouseEvent):void
    {
      if (!enabled)
        return;

      var sandboxRoot:DisplayObject = systemManager.getSandboxRoot();

      // Listener to catch mouse up all up events on the systemManager. We need
      // catpure = true to be sure to get them all.
      sandboxRoot.addEventListener(MouseEvent.MOUSE_UP, mouseUpHandler, true);
      
      // The above listener doesn't give use the up events outside the plugin
      // area so we also need a listener on the stage. Unfortunately puting capture =
      // true on that one would prevent us to get outside event! Thus the above
      // listener is still needed.mouseUp
      sandboxRoot.addEventListener(SandboxMouseEvent.MOUSE_UP_SOMEWHERE, mouseUpSomewhereHandler, true);
      // Listener to catch mouse up all move events on the systemManager. We need
      // catpure = true to be sure to get them all.
      sandboxRoot.addEventListener(MouseEvent.MOUSE_MOVE, mouseMoveHandler, true);       
      // The above listener doesn't give use the move events outside the plugin
      // area so we also need a listener on the stage. Unfortunately puting capture =
      // true on that one would prevent us to get outside event! Thus the above
      // listener is still needed.
      // We also need a specific listener to make sure we do not get twice
      // the move events when both systemManager & stage listeners are called.
      sandboxRoot.addEventListener(SandboxMouseEvent.MOUSE_MOVE_SOMEWHERE, mouseMoveSomewhereHandler); 
      
      _downX = e.stageX;
      _downY = e.stageY;
      _globalOrigin = localToGlobal(getOrigin());      
      _initialRot = MathUtil.pointAngleDeg(_downX, _downY, _globalOrigin.x, _globalOrigin.y);
      _orientationNum = scale.orientation=="clockwise"?-1:1;

      var a2:Number = rotation + MathUtil.pointAngleDeg(e.stageX, e.stageY, _globalOrigin.x, _globalOrigin.y);
      var ta:Number = MathUtil.toRadians(_initialRot + a2 - _initialRot + 180);
      updateValue(ta, false, true);      
    }

    /**
     *  @private
     */
    private function mouseMoveHandler(e:MouseEvent):void
    {
      mouseMoveHandlerImpl(e.stageX, e.stageY);
    }

    private function mouseMoveSomewhereHandler(e:MouseEvent):void
    {
      var p:Point = new Point(mouseX, mouseY);
      p = localToGlobal(p);
      mouseMoveHandlerImpl(p.x, p.y);      
    }

    private function mouseMoveHandlerImpl(stageX:Number, stageY:Number):void
    {
      if (!enabled)
        return;
        
      var a2:Number = rotation + MathUtil.pointAngleDeg(stageX, stageY, _globalOrigin.x, _globalOrigin.y);
      var ta:Number = MathUtil.toRadians(_initialRot + a2 - _initialRot + 180);
      
      updateValue(ta, false, false);        
    }

    /**
     *  @private
     */
    private function mouseUpSomewhereHandler(e:SandboxMouseEvent):void
    {
      var p:Point = new Point(mouseX, mouseY);
      p = localToGlobal(p);
      mouseUpHandlerImpl(p.x, p.y);      
    }

    /**
     *  @private
     */
    private function mouseUpHandler(e:MouseEvent):void
    {
      mouseUpHandlerImpl(e.stageX, e.stageY); 
    }

    private function mouseUpHandlerImpl(stageX:Number, stageY:Number):void
    {
      if (!enabled)
       return;      

      var sandboxRoot:DisplayObject = systemManager.getSandboxRoot();
      // Remove temporarly listeners.
      sandboxRoot.removeEventListener(MouseEvent.MOUSE_UP, mouseUpHandler, true);
      sandboxRoot.removeEventListener(SandboxMouseEvent.MOUSE_UP_SOMEWHERE, mouseUpSomewhereHandler, true);
      
      sandboxRoot.removeEventListener(MouseEvent.MOUSE_MOVE, mouseMoveHandler, true);
      sandboxRoot.removeEventListener(MouseEvent.MOUSE_MOVE, mouseMoveSomewhereHandler);  

      var a2:Number = rotation + MathUtil.pointAngleDeg(stageX, stageY, _globalOrigin.x, _globalOrigin.y);
      var ta:Number = MathUtil.toRadians(_initialRot + a2 - _initialRot + 180);            
      updateValue(ta, true, true);         
    }
    
    private function updateValue(angle:Number, forceCommit:Boolean, 
                                 animate:Boolean):void
    {
      angle = MathUtil.mod2PI(angle);
      // if we are not in liveDragging, or if we require continuous drawing
      // we *must* go through transitionPosition and not rely on the value
      // to redraw the value renderer. Of course not needed if we are in
      // forceCommit
      if ((editMode == "continuous" || !liveDragging) && !forceCommit) {
        _keepTransition = true;
        // just visually drag, this is not snapping if any snapping
        // is required => do first a snap trick
        if (animate && createTween() != null)
          animateTo(snapAngle(angle))
        else {
          // if an animation was going on let's stop it
          if (_animateProperty != null && _animateProperty.isPlaying) {
            _animateProperty.stop();           
          }
          transitionAngle = snapAngle(angle);
        }
      }      
      var newvalue:Object = scale.valueForAngle(angle);
      if (forceCommit || liveDragging && newvalue != value) {        
        if (forceCommit) {
          // we actually commit a value, so we do not need anymore transition position
          _keepTransition = false;
        }
        // if we are not asked to animate or if the animation already occured
        // in continuous drag above let's temporarily cancel animation
        if (!animate || ((editMode == "continuous" || !liveDragging) && !forceCommit)) {
          _preventAnimation = true;
          if (!animate && _animateProperty != null && _animateProperty.isPlaying) {
            _animateProperty.stop();
          }             
        }
        // actually set the value, snapping will be done if needed        
        value = newvalue;
        if (!animate || ((editMode == "continuous" || !liveDragging) && !forceCommit)) {
          _preventAnimation = false;
        }        
        if (forceCommit) {
          // make sure that after a force commit we have no more transition
          // value pending
          if (_animateProperty == null || !_animateProperty.isPlaying) {
            _transitionAngle = NaN;
          }         
        }           
        dispatchEvent(new Event(Event.CHANGE));        
      }               
    }    
    
    /**
     * Snaps the value if needed.
     * 
     * @param angle the angle to snap.
     * 
     * @return the snapped angle.
     * 
     * @see #liveDragging
     */
    private function snapAngle(angle:Number):Number
    {
      if (editMode == "discrete") {
        return scale.angleForValue(scale.valueForAngle(angle));
      } else {
        // do not snap if continuous mode, but still give it some limits
        if (scale.angleInRange(angle))
          return angle;
        else 
          return scale.angleForValue(scale.valueForAngle(angle));        
      } 
    }    

    [Inspectable(category="Gauge", defaultValue="false")]
    
    /**
     * Specifies whether live dragging is enabled for the element.
     * If <code>false</code>, the <code>value</code>
     * property is changed and a <code>"change"</code> 
     * event is dispatched when the user stops dragging the element.
     * If <code>true</code>,  the <code>value</code>
     * property is changed and a <code>"change"</code>
     * event is dispatched continuously as the user moves the element.
     *
     * @default false
     */
    public var liveDragging:Boolean = false;

    /**
     *  @private
     */
    private var _editMode:String = "discrete";
    
    [Inspectable(category="Gauge", enumeration="continuous,discrete")]    
    
    /**
     * The interaction mode when editing the value. If <code>"discrete"</code>, 
     * on each user mouse move, the value renderer snaps to the scale accepted
     * values, otherwise the value renderer moves continuously.
     *  
     * @default "discrete"
     * @see ilog.gauges.gaugesClasses.LinearScale#snapInterval 
     */     
    public function get editMode():String
    {
      return _editMode;
    }

    /**
     *  @private
     */
    public function set editMode(value:String):void
    {
      _editMode = value;
    }
    
    /**
     *  @private
     */
    private var _mouseMode:String = "shape";    

    [Inspectable(category="Gauge", enumeration="shape,area")]    
    
    /**
     * Specifies whether only the shape of the renderer reacts to mouse interaction. If 
     * <code>"area"</code> is set, mouse interaction is enabled in a circle 
     * defined by the origin and radius of the renderer. If <code>"shape"</code>
     * is set, mouse interaction is enabled on the shape of the value
     * renderer.
     * This property has no effect if <code>editable</code> is set to <code>false</code>.
     * 
     * @default "shape"
     */     
    public function get mouseMode():String
    {
      return _mouseMode;
    }

    /**
     *  @private
     */
    public function set mouseMode(value:String):void
    {
      _mouseMode = value;
      invalidateDisplayList();
    }

    /**
     *  @private
     */
    private var _animationOrientation:String = "normal";    

    [Inspectable(category="Gauge", enumeration="normal,inverse")]
        
    /**
     * The orientation of the animation. Valid values are
     * <code>"normal"</code> and <code>"inverse"</code>. If it is set to 
     * <code>"normal"</code>, the animation orientation is the same as the scale 
     * orientation.
     *  
     * @default "normal"
     */     
    public function get animationOrientation():String
    {
      return _animationOrientation;
    }

    /**
     *  @private
     */
    public function set animationOrientation(value:String):void
    {
      _animationOrientation = value;
    }

    private function createTween():AnimateProperty
    {
      var animationDuration:Number = getStyle("animationDuration");
      if (animationDuration > 0) {
        if (_animateProperty == null) { 
          _animateProperty = new AnimateProperty(this);
          _animateProperty.property = "transitionAngle";
          _animateProperty.addEventListener(EffectEvent.EFFECT_END, endTween);
        }
        setupTween(animationDuration, getStyle("easingFunction"));
      } else {
        // reset it
        _animateProperty = null;
      }
      return _animateProperty; 
    }
    
    private function setupTween(animationDuration:Number, easingFunction:Function):void
    {
      if (_animateProperty != null) {
        _animateProperty.duration = animationDuration;
        _animateProperty.easingFunction = easingFunction;
      }
    }
    
    private function endTween(e:Event):void
    {
      if (!_keepTransition) {
        _transitionAngle = NaN;
      }
    }

    /**
     *  @private
     */
    private var _transitionAngle:Number = NaN;
    
    
    [Inspectable(environment="none")]
    [Bindable("transitionAngleChanged")]
    /**
     * The transition angle used during animation and interaction. It is set to 
     * <code>NaN</code> when the element is not animated or the user is not 
     * interacting. This property must be used by subclasses to temporarily update
     * their display during animation or interaction. Subclasses typically
     * do:
     * <pre>
     * protected override function updateDisplayList(unscaledWidth:Number, unscaledHeight:Number):void
     * {
     *    var angle:Number;
     *    if (isNaN(transitionAngle)) {
     *       // use the value
     *       angle = scale.angleForValue(value);
     *    } else {
     *       angle = transitionAngle;
     *    }
     *    // draw something depending on angle
     * } 
     * </pre>
     */     
    public function get transitionAngle():Number
    {
      return _transitionAngle;
    }

    /**
     *  @private
     */
    public function set transitionAngle(value:Number):void
    {
      _transitionAngle = MathUtil.mod2PI(value);
      invalidateDisplayList();
      dispatchEvent(new Event("transitionAngleChanged"));
    }
    
    private var _delegate:CircularGaugeElement;
    
    [Inspectable(category="Gauge")]
    
    /**
     * A delegate <code>CircularGaugeElement</code> which will be 
     * in charge of rendering the value of the <code>CircularValueRenderer</code>.
     * This is useful when you want to use a <code>CircularGaugeElement</code>
     * that does not inherit from <code>CircularValueRenderer</code> as the one
     * in charge of rendering the value.
     * 
     * @since ILOG Elixir 2.0
     * @ilog.reviewed SH 5 May 08
     */     
    public function get delegate():CircularGaugeElement
    {
      return _delegate;
    }

    /**
     * @private
     */
    public function set delegate(value:CircularGaugeElement):void
    {
      _delegate = value;
      invalidateDisplayList();
    }     

    /**
     *  @private
     */    
    override protected function updateDisplayList(unscaledWidth:Number, unscaledHeight:Number):void
    {
      super.updateDisplayList(unscaledWidth, unscaledHeight);      
      // mouse shield
      if (editable == true && mouseMode == "area") {
        var o:Point = getOrigin();
        var r:Number = getMouseRadius();
        graphics.beginFill(0x000000, 0);
        graphics.drawCircle(o.x, o.y, r);
        graphics.endFill();
      }
      if (delegate) {
        delegate.invalidateDisplayList();
      }
    }

    /**
     * @private
     */
    private function getMouseRadius():Number
    {
      if (! isNaN(mouseRadius))
        return mouseRadius;
      else
        return percentMouseRadius * getRadius() / 100;
    }  

    /**
     *  @private
     */
    private var _mouseRadius:Number = NaN;

    [Bindable("mouseRadiusChanged")]
    [Inspectable(category="Gauge")]
    [PercentProxy("percentMouseRadius")]

   /**
    * This property specifies the radius in pixels that is reactive to mouse events.
    * This property is only used if <code>mouseMode</code> is true.
    * If a percentage is found, it is expressed relative to the total radius.
    *  
    * <p><strong>Note:</strong> You can specify a percentage value in the MXML
    * <code>mouseRadius</code> attribute, such as <code>radius="110%"</code>,
    * but you cannot use a percentage value in the <code>radius</code>
    * property in ActionScript.
    * Use the <code>percentMouseRadius</code> property instead.</p>
    * 
    * @since ILOG Elixir 2.0
    * @default NaN
    * @ilog.reviewed SH 27 July 08
    */    
    public function get mouseRadius():Number
    {
      return _mouseRadius;
    }

    /**
     *  @private
     */
    public function set mouseRadius(value:Number):void
    {
      _mouseRadius = value;
      if (! isNaN(value))
        _percentMouseRadius = NaN;      
      invalidateDisplayList();      
    }

    /**
     *  @private
     */
    private var _percentMouseRadius:Number = 100;

    [Bindable("resize")]
    [Inspectable(environment="none")]

    /**
     * Number that specifies the <code>mouseRadius</code> length as a percentage.
     * @see mouseRadius
     * @since ILOG Elixir 2.0 
     * @default 100
     * @ilog.reviewed SH 27 July 08
     */
    public function get percentMouseRadius():Number
    {
      return _percentMouseRadius;
    }

    /**
     *  @private
     */
    public function set percentMouseRadius(value:Number):void
    {
      _percentMouseRadius = value;
      if (! isNaN(value))
        _mouseRadius = NaN;      
      invalidateDisplayList();
    }
  }
}
